/*
 * Copyright 2009-2010 Nanjing RedOrange ltd (http://www.red-orange.cn)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package redora.configuration.rdo.service.base;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import redora.configuration.rdo.model.RedoraConfiguration;
import redora.configuration.rdo.service.RedoraConfigurationService;
import redora.configuration.rdo.service.ServiceFactory;
import redora.exceptions.*;
import redora.service.BusinessRuleViolation;
import redora.service.ServiceBase;
import redora.util.ResourceFileHandler;

import javax.servlet.ServletContext;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import static java.util.logging.Level.*;
import static redora.api.fetch.Page.ALL_TABLE;
import static redora.configuration.rdo.model.base.RedoraConfigurationBase.Status.Error;
import static redora.configuration.rdo.model.base.RedoraConfigurationBase.Status.*;

/**
 * Base class for bridging your object model and the database tables.
 * In your application in the 'target/generated-sources/redora-target' directory
 * you can find the ...rdo.service.Upgrade class. This Upgrade extends this base class.
 * Usages
 * <br><br>
 * Create Tables<br>
 * In all you have functionality to create tables based on the model (see ..rdo.sql(and .base)
 * for the CREATE_TABLE statement.
 * <br><br>
 * Definition Check<br>
 * There is a function that checks if the table definition in the database is still in sync
 * with your model: see ..rdo.service.check for the PojoCheck classes. For example you might have changed
 * your model but have forgotten to add an alter table script.
 * <br><br>
 * Running Upgrade Scripts<br>
 * The upgrade function implemented in Upgrade and UpgradeBase. All upgrade scripts,
 * files located in /src/main/resources/upgrade are executed once for you application. Redora will
 * maintain the RedoraConfiguration table with a list of upgrade scripts and their execution status.
 *
 * @author Nanjing RedOrange (http://www.red-orange.cn)
 */
public abstract class UpgradeBase extends ServiceBase {

    static final transient Logger l = Logger.getLogger("redora.configuration.rdo.service.base.UpgradeBase");

    public final static String[] redoraTables = {"RedoraConfiguration", "RedoraTrash"};

    public abstract List<String> objects();

    public abstract String[] relationTables();

    public abstract String[] scripts();

    /**
     * Only access this class through the Upgrade class in your application located
     * in ..rdo.service in the generated-sources target.
     *
     * @throws ConnectException On failing to get a DB connection
     */
    protected UpgradeBase(@NotNull String schema) throws ConnectException {
        super(schema);
    }

    public void upgradeTables(@Nullable PrintWriter out, @Nullable ServletContext context)
            throws RedoraException, IOException {

        executeUpgradeScript(context);

        boolean retVal;
        ResultSet rs = null;
        try {
            rs = st.con.con.getMetaData().getTables(null, null, null, null);
            while (rs.next()) {
                retVal = false;
                for (String tableName : objects()) {
                    if (rs.getString("TABLE_NAME").equalsIgnoreCase(tableName)) {
                        retVal = true;
                        break;
                    }
                }
                for (String tableName : relationTables()) {
                    if (rs.getString("TABLE_NAME").equalsIgnoreCase(tableName)) {
                        retVal = true;
                        break;
                    }
                }
                for (String tableName : redoraTables) {
                    if (rs.getString("TABLE_NAME").equalsIgnoreCase(tableName)) {
                        retVal = true;
                        break;
                    }
                }
                if (!retVal) {
                    l.log(WARNING, "Table {0} does not exist in the model, but i did find it in the database", rs.getString("TABLE_NAME"));
//                    if (out != null) {
//                        out.println("Warning: table " + rs.getString("TABLE_NAME") + " does not exist in the model, but i did find it in database." + "<br>");
//                    }
                }
            }
        } catch (SQLException e) {
            l.log(SEVERE, "Failed to open meta data", e);
            throw new QueryException("Failed to open meta data", e);
        } finally {
            close(rs);
        }
    }

    protected void executeUpgradeScript(@Nullable ServletContext context) throws RedoraException {
        initUpgradeScript();

        RedoraConfigurationService service = ServiceFactory.redoraConfigurationService();
        ResultSet rs = null;
        try {
            for (RedoraConfiguration conf : service.findByStatus(New, ALL_TABLE)) {
                String currentScript = null;
                try {
                    for (String sql : loadSql(conf.getScriptName(), context)) {
                        currentScript = sql;
                        execute(currentScript);
                        l.log(INFO, "Adding SQL statement: {0}", currentScript);
                    }
                    conf.setStatus(Ready);
                    if (!service.persist(conf).isEmpty()) {
                        throw new BusinessRuleViolationException("Failed to persist RedoraConfiguration due to a business rule violation (" + conf + ")");
                    }
                } catch (QueryException e) {
                    l.log(WARNING, "Failed to perform upgrade script " + currentScript + " from upgrade file: " + conf.getScriptName(), e);
                    conf.setStatus(Error);
                    String error = "Failed with this script\r\n" + currentScript + "\r\nWith error:\r\n" + e.getMessage();
                    try {
                        conf.setOutput(error);
                        if (!service.persist(conf).isEmpty()) {
                            throw new BusinessRuleViolationException("Failed to persist RedoraConfiguration due to a business rule violation (" + conf + ")");
                        }
                    } catch (PersistException e1) {
                        l.log(SEVERE, "Failed to persist configuration " + error, e1);
                    }
                }
            }
        } catch (IOException e) {
            l.log(SEVERE, "Failed", e);
        } finally {
            close(rs);
            ServiceFactory.close(service);
        }
    }

    /**
     * Scans the SCRIPT_SOURCE_DIR for new upgrade scripts. Adds them to RedoraConfiguration.
     *
     * @throws RedoraException New scripts are added to the RedoraConfiguration table. This exception
     *                         is thrown when failing to manipulate this table.
     */
    protected void initUpgradeScript() throws RedoraException {
        RedoraConfigurationService service = null;
        try {
            service = ServiceFactory.redoraConfigurationService();
            for (String upgrade : scripts()) {
                if (service.findByScriptName(upgrade, ALL_TABLE).isEmpty()) {
                    RedoraConfiguration conf = new RedoraConfiguration();
                    conf.setScriptName(upgrade);
                    Set<BusinessRuleViolation> brViolations = service.persist(conf);
                    if (!brViolations.isEmpty()) {
                        String s = "One or more business rules have been violated: ";
                        for (BusinessRuleViolation br : brViolations) {
                            s += br.getPersistable().getClass().getName() + " id: " + br.getPersistable().getId() + " for rule: " + br.getBusinessRuleId();
                        }
                        l.log(SEVERE, "Failed to persist new configuration for script {0}", upgrade);
                        throw new BusinessRuleViolationException(s);
                    }
                    l.log(INFO, "Found new script file {0}", conf.getScriptName());
                }
            }
        } finally {
            redora.configuration.rdo.service.ServiceFactory.close(service);
        }
    }


    /**
     * Locates the script file and makes a list of sql statements in the file.
     * Each SQL statement is supposed to end with ;.
     *
     * @param sqlFile (Mandatory) File name of the upgrade script without path
     * @param context (Optional) The servlet container (if applicable) for classpath reference to load the file
     * @return Empty or filled list with SQL statements.
     * @throws IOException When loading the script file fails/
     */
    @NotNull
    List<String> loadSql(@NotNull String sqlFile, @Nullable ServletContext context) throws IOException {
        List<String> retVal = new ArrayList<String>();

        InputStream in = ResourceFileHandler.findUpgradeFile(sqlFile, context);
        if (in == null) {
            throw new IOException("Did not find upgrade file " + sqlFile);
        }
        String upgradeFile = IOUtils.toString(in, "utf-8");
        in.close();

        String[] sqlStatements = upgradeFile.split("(;\\s*\\r\\n)|(;\\s*\\n)");
        for (String sql : sqlStatements) {
            sql = sql.replaceAll("--.*", "").trim();
            sql = sql.replaceAll("#.*", "").trim();
            if (StringUtils.isNotEmpty(sql)) {
                retVal.add(sql);
            }
        }
        return retVal;
    }

    protected void checkRelationTable(@NotNull String tableName, @NotNull String objectA
            , @NotNull String objectB, @NotNull PrintWriter out)
            throws ConnectException, QueryException {
        ResultSet rs = null;
        try {
            rs = st.con.con.getMetaData().getColumns("", "", tableName, "");
            HashMap<String, HashMap<String, Object>> map = new HashMap<String, HashMap<String, Object>>();
            while (rs.next()) {
                HashMap<String, Object> list = new HashMap<String, Object>();
                list.put("COLUMN_NAME", rs.getString("COLUMN_NAME"));
                list.put("DATA_TYPE", rs.getInt("DATA_TYPE"));
                map.put(rs.getString("COLUMN_NAME"), list);
            }
            for (String key : map.keySet()) {
                String COLUMN_NAME = map.get(key).get("COLUMN_NAME").toString();
                if (!(COLUMN_NAME.equalsIgnoreCase(objectA) || COLUMN_NAME.equalsIgnoreCase(objectB)
                        || COLUMN_NAME.equalsIgnoreCase("creationDate") || COLUMN_NAME.equalsIgnoreCase("updateDate") || COLUMN_NAME.equalsIgnoreCase("roDeleted"))) {
                    l.log(WARNING, " {0} does not have {1} attribute but it exists in database", new Object[]{tableName, key});
                    if (out != null) {
                        out.print(tableName + " does not have " + key + " attribute but it exists in database.<br>");
                    }
                }
            }
            if (!map.containsKey(objectA)) {
                l.log(WARNING, "{0}.{1} does not existed or its sqltype does not match according to xml", new Object[]{tableName, objectA});
                if (out != null) {
                    out.print(tableName + "." + objectA + " does not existed or its sqltype does not match according to xml.<br>");
                }
            }
            if (!map.containsKey(objectB)) {
                l.log(WARNING, "{0}.{1} does not existed or its sqltype does not match according to xml", new Object[]{tableName, objectB});
                if (out != null) {
                    out.print(tableName + "." + objectB + " does not existed or its sqltype does not match according to xml.<br>");
                }
            }
            if (!map.containsKey("creationDate")) {
                l.log(WARNING, "{0}.creationDate does not existed or its sqltype does not match according to xml", tableName);
                if (out != null) {
                    out.print(tableName + ".creationDate does not existed or its sqltype does not match according to xml.<br>");
                }
            }
            if (!map.containsKey("updateDate")) {
                l.log(WARNING, "{0}.updateDate does not existed or its sqltype does not match according to xml", tableName);
                if (out != null) {
                    out.print(tableName + ".updateDate does not existed or its sqltype does not match according to xml.<br>");
                }
            }
        } catch (SQLException e) {
            l.log(SEVERE, "Failed to execute checkRelationTable " + tableName, e);
            throw new QueryException("Failed to execute checkRelationTable " + tableName, e);
        } finally {
            close(rs);
        }
    }

    @Override
    public void close() {
        super.close();
    }
}
